home *** CD-ROM | disk | FTP | other *** search
/ Amiga Developer CD 2.1 / Amiga Developer CD v2.1.iso / Reference / Amiga_Mail_Vol2 / Archives / Plain / jf92 / UserShell / UserShell.txt < prev   
Encoding:
Text File  |  1992-01-03  |  18.0 KB  |  367 lines

  1. (c)  Copyright 1992 Commodore-Amiga, Inc.   All rights reserved.
  2. The information contained herein is subject to change without notice,
  3. and is provided "as is" without warranty of any kind, either expressed
  4. or implied.  The entire risk as to the use of this information is
  5. assumed by the user.
  6.  
  7.  
  8. Writing a UserShell
  9.  
  10. by Randell Jesup
  11.  
  12.  
  13. One of the features of Release 2.0 is that the OS allows the user to change the
  14. system default shell, or the UserShell.  Any time the user opens a shell with
  15. NewShell, executes a script, RUNs a command, or indirectly calls System() with
  16. SYS_UserShell, the OS will call the UserShell instead of the BootShell (by
  17. default the system sets up the BootShell as the UserShell).
  18.  
  19. Creating UserShells is not easy, and requires doing a fairly large number of
  20. things for no apparent reason (the reasons are there, they're just not obvious
  21. to the outsider).  This article will attempt to give you the information you
  22. need in order to create a usable, system-friendly UserShell.
  23.  
  24.  
  25. Initialization
  26.  
  27. The entity that starts the shell calls the shell code in C style (RTS to exit).
  28. This entity also sends a startup packet to your process port.  You must
  29. retrieve this packet before doing any DOS I/O (much like WBMessages).  You can
  30. use WaitPkt() for this.  The entity will take care of attaching a
  31. CommandLineInterface structure to your process, which will be freed on exit
  32. from the UserShell by the system.
  33.  
  34. In your process structure, check the SegArray pointed to by pr_Seglist (note
  35. that it's a BPTR).  If SegArray[4] is NULL, you must move the value from
  36. SegArray[3] to SegArray[4], and NULL out SegArray[3].  This is because
  37. SegArray[3] will be used to store the seglist pointer for each program you run.
  38.  
  39. The startup packet contains some information that tells the UserShell what kind
  40. of shell to be.  At present, the two sets of sources can launch the UserShell:
  41.  
  42.     The Run command, Execute(), or System()
  43.     The NewShell or NewCLI resident commands
  44.  
  45. The size of the stack that the system gives the UserShell depends on how the
  46. user started the shell.  If it was started from Execute() or System(), the
  47. stack is 3200 bytes.  If the UserShell was started from Run, NewShell, or
  48. NewCLI, the stack is 4000.
  49.  
  50. The type of shell required is specified by the combination of the packet's
  51. dp_Res1 and dp_Res2 fields.   Here's a piece of code for turning them into a
  52. value from 0 to 3:
  53.  
  54.     init_type = (parm_pkt->dp_Res1 == 0 ? 0:2)|(parm_pkt->dp_Res2 == 0 ? 0:1);
  55.  
  56. Currently, only types 0 and 2 are implemented.  For 1 and 3 you should exit
  57. with an error (returning the packet).  Type 0 is for Run, Execute() and
  58. System(), type 2 is for NewShell and NewCLI.  After setting up your SegArray as
  59. above, for type 0 call CliInitRun(pkt), and for type 2 call CliInitNewcli(pkt).
  60. These both return a value we'll call ``fn''.  Keep fn around, it has useful
  61. state information that you'll need later.  Note that these CliInitXxxx
  62. functions don't follow the normal DOS convention of Dn for arguments (they use
  63. A0 for pkt!).
  64.  
  65. The CliInitXxxx functions do many magic things to get all the streams and
  66. structures properly set up, etc.  You shouldn't need to know anything about
  67. this or what the values in the packet are, other than dp_Res1 and dp_Res2 (see
  68. the Appendix for more information on these functions).
  69.  
  70.            Definitions for the values of fn:
  71.  
  72. Bit 31    Set to indicate flags are valid
  73. Bit  3     Set to indicate an asynchronous System() call
  74. Bit  2     Set if this is a System() call
  75. Bit  1     Set if user provided input stream
  76. Bit  0     Set if RUN provided output stream
  77.  
  78. If fn bit 31 is 0 (fn >= 0), then you must check IoErr() to find out what to
  79. do.  If IoErr() is a pointer to your process, there has been an error in the
  80. initialization of the CLI structure and processing the packet.  In this case
  81. you should clean up and exit.  You don't have to return the packet because the
  82. CliInitXxxx functions take care of this for you if there is an error.  If
  83. IoErr() isn't a pointer to your process, then if this is a NewCLI or NewShell
  84. command (init_type of 2), reply the packet immediately.
  85.  
  86. If the init_type is 0, you have to look at fn to determine when to send back
  87. the startup packet.  If the shell was called from an asynchronous System()
  88. function ((fn & 0x8000000C) == 0x8000000C), return the packet immediately.  If
  89. this is a synchronous System() call ((fn & 0x8000000C) == 0x80000004) or the fn
  90. flags are valid but this is not a System() call ((fn & 0x8000000C) ==
  91. 0x80000000) (Execute() does this), you return the packet just before exiting
  92. from your shell (see the Cleanup section below).  If the fn flags are invalid
  93. (bit 31 == 0), but there is something other than your task pointer in IoErr(),
  94. then this shell was called by the Run command.  Here you can either return the
  95. packet immediately, or return it after having loaded the first command (or
  96. failed to find/load it).  This delay in reply helps avoid the disk thrashing
  97. caused by two commands loading at the same instant.
  98.  
  99. When you do a ReplyPkt(), use ReplyPkt(pkt, pkt->dp_Res1, pkt->dp_Res2) to
  100. avoid losing error codes set up by CliInitXxxx.
  101.  
  102. Initialize pr_HomeDir to NULL, set up any local shell variables, etc.
  103.  
  104. We're all set up now, so you can now enter your main loop and start taking
  105. commands.
  106.  
  107.  
  108. A Word About the Shell's I/O Handles
  109.  
  110. There are three pairs of I/O handles in a shell process.  The shell's Process
  111. structure contains the pr_CIS (current input stream) and pr_COS (current output
  112. stream) file handles.  That Process's CommandLineInterface structure contains
  113. the other two pairs of I/O handles: cli_StandardInput/cli_StandardOutput and
  114. cli_CurrentInput/cli_CurrentOutput.  Each has different uses within a normal
  115. shell.
  116.  
  117. Routines that operate on Input() or Output(), such as ReadArgs() or ReadItem(),
  118. use the pr_CIS and pr_COS I/O handles (which they acquire by calling the
  119. dos.library routines Input() and Output(), not by directly looking at the
  120. Process structure).  Shell-launched application programs the run on the shell's
  121. process also use these I/O handles as their normal input and output channels.
  122. This is where functions like scanf() and printf() get and send their input and
  123. output.  The shell changes these file handles (using
  124. SelectInput()/SelectOutput()) according to the shell defaults and according to
  125. any I/O redirection.
  126.  
  127. The cli_StandardInput and cli_StandardOutput I/O handles are the default input
  128. and output channels for the shell.  They usually refer to the user's console
  129. window and will not change while the shell is running.  The shell should use
  130. these values as the default values for pr_CIS and pr_COS (via SelectInput() and
  131. SelectOutput()) when it runs a command from a command line.
  132.  
  133. The cli_CurrentInput handle is the current source of command lines.  This
  134. normally is the same as cli_StandardInput.  The cli_CurrentInput handle will
  135. differ from cli_StandardInput when the shell is executing a script or when
  136. handling an Execute() or System() call.  In these cases, it points to a file
  137. handle from which the shell is reading commands.  This handle refers to one of
  138. three files: the script file you called with the execute command, a temporary
  139. file created by the execute command, or a pseudo file created by Execute() or
  140. System().
  141.  
  142. When a shell runs the execute command, If cli_CurrentInput differs from
  143. cli_StandardInput, The execute command will close cli_CurrentInput and replace
  144. it with a new one, so don't cache the value of cli_CurrentInput as it will be
  145. invalid.  In this case, cli_CurrentInput must not be the same as pr_CIS when
  146. you call RunCommand() if the executable could possible be the execute commands
  147. (or anything else that tries to close cli_CurrentInput).
  148.  
  149. The cli_CurrentOutput file handle is currently unused by the system.  It's
  150. initialized to the same as cli_StandardOutput.
  151.  
  152.  
  153. The Main Shell Loop
  154.  
  155. Note: some things in this section assume your UserShell will act similarly to
  156. the Boot Shell in 2.0.  If not, modify to see fit, but pay close attention to
  157. things external programs will notice (such as the setup of the process and CLI
  158. structures).  In particular, the article assumes that you handle scripts by
  159. redirecting cli_CurrentInput to a file with the script in it, as the execute
  160. command does.  Note that the execute command will attempt to do this if you run
  161. it--be careful.
  162.  
  163. Before reading a command line, you need to SelectInput() on the I/O handle in
  164. the current cli_CurrentInput, and SelectOutput() on cli_StandardOutput.  This
  165. makes sure the shell is reading from its command line source and writing to the
  166. the default output handle.
  167.  
  168. If this shell is executing a script, you should check if the user hit the break
  169. key for scripts (Ctrl-D is what the BootShell uses).  If you do detect a script
  170. break, you can print an error message to the current output stream by calling
  171. PrintFault(304, "<your shell name>").  304 is the error number (ERROR_BREAK)
  172. and the string gets prepended to the error message (which is currently "
  173. :***Break").  This allows the OS to print the error message using the standard
  174. error message which can be internationalized in future versions of the OS.
  175.  
  176. Next, determine if you should print a prompt.  The nasty statement below sets
  177. up the Interactive flag for you, by setting it if the following are true:
  178.  
  179. This shell is not a background shell
  180. input has not been redirected to an Execute/script file
  181. this is not a System() call
  182.  
  183. You don't have to handle it precisely this way, but this works (Note:
  184. 0x80000004 is a test for whether this is a System() call, see the ``fn'' bit
  185. definitions above).
  186.  
  187. #define SYSTEM          ((((LONG)fn) & 0x80000004) == 0x80000004)
  188. #define NOTSCRIPT       (clip->cli_CurrentInput == clip->cli_StandardInput)
  189.  
  190.          clip->cli_Interactive = (!clip->cli_Background && NOTSCRIPT && !SYSTEM) ?
  191.                                  DOSTRUE : FALSE;
  192.  
  193. The BootShell prints a prompt if cli_Interactive is DOSTRUE.
  194.  
  195. Do all your mucking with the input line, alias and local variable expansion,
  196. etc.
  197.  
  198.  
  199. Finding a Program
  200.  
  201. There are several possible places a shell can look for commands passed to it.
  202. The resident list is an important place to look as it contains many commands
  203. that the user finds important enough to keep loaded into memory at all times.
  204. Some shells have commands built directly into them.  Of course, if the shell
  205. cannot find a command in the resident list or in its collection of internal
  206. commands, the shell has to scan the path looking for the command.  If a shell
  207. supports the script bit, when it finds a command on disk with the script bit
  208. set, it should read commands from the script file.
  209.  
  210. Here's how you deal with commands on the resident list: After finding the
  211. command (under Forbid()!), if the Segment structure's seg_UC is >= 0, increment
  212. it; if less than 0, don't modify it.  If seg_UC is less than CMD_DISABLED, the
  213. corresponding resident command is currently disabled and you should not execute
  214. it.  The same is true if seg_UC is equal to CMD_SYSTEM.  After incrementing
  215. seg_UC, you can Permit().  After using a resident command, decrement the seg_UC
  216. count if it's greater than 0 (under Forbid() again).
  217.  
  218. When identifying scripts, I advise that you use something unique to identify
  219. your scripts, and pass all other scripts to the Boot Shell via System() for
  220. execution.  A good method (which was worked out on BIX long ago) is to include
  221. within the first 256 characters or so, the string "#!<your shell name, ala
  222. csh>!#".  BootShells could, for example, start with "; #!c:execute!#.  The idea
  223. is the string inside the #!...!# should be the interpreter to run on the
  224. script.  If none is specified, give it to the BootShell.  If you want, you
  225. could extend this to include handling of the sequence for all interpreters.
  226. The programs should be invoked as "<interpreter> <filename> <args>" as if the
  227. user had typed that.
  228.  
  229. Don't forget to set pr_HomeDir for programs loaded from disk.  The Lock in
  230. pr_HomeDir should be a DupLock() of the directory the program was loaded from.
  231. For programs from the resident list, leave it NULL.
  232.  
  233. Please support multi-assigned C: directories.  The important thing here is to
  234. not lock C:.  Instead, prepend ``C:'' onto the filename you wish to
  235. Lock()/LoadSeg().  Also, if a command is loaded from C:, get its pr_HomeDir by
  236. Lock()ing the file (with C: prepended), and then using ParentDir() to get its
  237. home directory.
  238.  
  239. The Path is attached to cli_CommandDir.  It is a BPTR to a NULL terminated,
  240. singly-linked list (connected via BPTRs) of directory Locks:
  241.  
  242.     struct pathBPTRlistentry {
  243.         BPTR   pathBPTRlistentry *next;
  244.         struct Lock              directorylock
  245.     }
  246.  
  247. Please don't modify the list; use the Path command instead.  This will make it
  248. far easier for us to improve this in the future.
  249.  
  250. Make sure you clear the SIGBREAK_CTRL_x signals before starting a program.  In
  251. order to prevent the user from hitting a break key somewhere between where you
  252. check for the break and where you clear the signals (thus losing the break
  253. signal), you may wish check for a break and clear the signals at the same time.
  254. The safest way is to use:
  255.  
  256. oldsigs = SetSignal(0L, SIGBREAK_CTRL_C |
  257.                         SIGBREAK_CTRL_D |
  258.                         SIGBREAK_CTRL_E |
  259.                         SIGBREAK_CTRL_F);
  260.  
  261. Then you can check oldsigs for any signals that you care about.
  262.  
  263.  
  264. Running a Program
  265.  
  266. To actually invoke a program on your process, use RunCommand()--it does special
  267. compatibility magic that keeps certain third-party applications working
  268. properly.   If RunCommand() fails due to lack of memory, it returns -1
  269. (normally success!).  In this case, check IoErr().  If it is equal to
  270. ERROR_NO_FREE_STORE, then RunCommand() really ran out of memory.  Note that
  271. RunCommand() stuffs a copy of your arguments into the buffer of the input
  272. handle for use by ReadArgs(), and un-stuffs them when the program exits.  Also
  273. note that RunCommand() takes stack size in bytes, and cli_DefaultStack is the
  274. size of the stack in LONGs.
  275.  
  276. After the program has completed, free the Lock in pr_HomeDir and set it to
  277. NULL.  Re-setup your I/O handles with SelectInput() on cli_CurrentInput and
  278. SelectOutput() on cli_StandardOutput.  It's a good idea to NULL cli_Module here
  279. as well, as it can make your exit/cleanup logic easier.
  280.  
  281. You must eat any unused buffered input.  Here's some tricky code that does that
  282. (read the Autodocs to figure it out if you wish):
  283.  
  284.         ch = UnGetC(Input(),-1) ? 0 : '\n';
  285.         while ((ch != '\n') && (ch != END_STREAM_CH)) ch = FGetC(Input());
  286.  
  287. Note: ENDSTREAMCH is EOF (-1).  Newer include files #define this in
  288. <dos/stdio.h> and <dos/stdio.i>.
  289.  
  290. To finish the main input loop, use the code below or something like it.  This
  291. keeps compatibility with certain hacks people had figured out about 1.3 (See
  292. SYSTEM and NOTSCRIPT #defines above).
  293.  
  294.             /* for compatibility */
  295.             /* system exit special case - taken only if they have played */
  296.             /* around with the input stream */
  297.             if (SYSTEM && NOTSCRIPT) break;
  298.         } while (ch != ENDSTREAMCH);
  299.  
  300. EndCLI sets fh_End = 0, which causes FGetC() to call replenish(), which returns
  301. ENDSTREAMCH on fh_End == 0.  EndCLI also sets cli_Background!  This neatly
  302. avoids a prompt after EndCLI.
  303.  
  304. After you've gotten an EOF (falling out of the while(ch != ENDSTREAMCH)
  305. statement above), you need to check if the shell was executing a script file.
  306. For Execute-type scripts, if (cli_CurrentInput != cli_StandardInput) is true,
  307. the shell was executing a script.  If this is the case, you'll need to Close()
  308. the cli_CurrentInput, and DeleteFile() the temporary file cli_CommandFile, if
  309. there is a file name there.  Next, set cli_CurrentInput to cli_StandardInput,
  310. and restore the fail level.  Then you go back to your normal input loop and
  311. accept commands again.  Note: this is based on handling scripts in the same
  312. manner as the BootShell--you may handle them in different ways.
  313.  
  314. On EOF, you also need to check if this is a System() call.  The check for a
  315. System() call is ((fn & 0x80000004) == 0x80000004).  If you had been handling a
  316. System() call, or if the shell was not executing a script, you should go to
  317. your cleanup code to exit.
  318.  
  319.  
  320. Cleanup
  321.  
  322. If you're exiting, use fn to tell you what to close, etc.  First check if fn
  323. contains valid flags ((fn & 0x80000000) != 0)).  If it does not have valid
  324. flags, Flush() and Close() cli_StandardOutput (if non-NULL), and Close()
  325. cli_StandardInput (if non-NULL).  If fn does contain valid flags,
  326. Flush(Output()), then check the other flags in fn.  If (fn&2 == 0) (if the user
  327. didn't provide an input stream), Close() cli_StandardInput.  If (fn&1 == 1) (if
  328. Run provided an output stream), Close() cli_StandardOutput (note, this is
  329. opposite the previous flag!)  If (fn&8 == 0) (if this is not an asynchronous
  330. System() call), you still have to ReplyPkt() the initial packet.  Before
  331. sending back the packet put cli_ReturnCode in the packet's result1 and
  332. cli_Result2 in the packet's result2 (i.e. return the result of the last program
  333. run if this was a synchronous System() or Execute() call).
  334.  
  335. In cleanup, unlock pr_CurrentDir and set it to NULL, free up anything you
  336. allocated, and exit!  The system will take care of your CommandLineInterface
  337. structure, and anything else it allocated.
  338.  
  339.  
  340. Installing the New User Shell
  341.  
  342. After you have compiled your creation, you need to put its seglist on the
  343. resident list under the name ``shell''.  Adding it to the resident list is a
  344. simple:
  345.  
  346.     Resident shell <shell-file> SYSTEM
  347.  
  348. Now anything that calls the user shell (like NewShell, Execute(), and System())
  349. will call your shell.  Note that under 2.04, the Shell icon actually runs
  350. sys:System/CLI, which calls the BootShell (the default UserShell) and not the
  351. current UserShell.
  352.  
  353. If you need to restore the BootShell as the UserShell, compile and run the
  354. program RestoreShell.c at the end of this article
  355.  
  356.  
  357. Credits
  358.  
  359. I thank greatly the input and bug reports I got from Bill Hawes during the
  360. development of the 2.0 DOS and it's shell interface.  It's still extremely
  361. ungainly, but it is now usable.
  362.  
  363. I also thank Michael B. Smith and Jesper Steen Moller for taking the initial
  364. confusing Usenet article I posted and making working shells from that
  365. information, and John Orr for making me write this article and producing the
  366. example (which helped make me clean up confusing points in it).
  367.